Utforska TypeScript branded types, en kraftfull teknik för att uppnÄ nominell typning i ett strukturellt typsystem. LÀr dig hur du förbÀttrar typsÀkerhet och kodtydlighet.
TypeScript Branded Types: Nominell typning i ett strukturellt system
TypeScript's strukturella typsystem erbjuder flexibilitet men kan ibland leda till ovÀntat beteende. Branded types Àr ett sÀtt att upprÀtthÄlla nominell typning, vilket förbÀttrar typsÀkerheten och kodens tydlighet. Denna artikel utforskar branded types i detalj, med praktiska exempel och bÀsta praxis för deras implementering.
FörstÄ strukturell vs. nominell typning
Innan vi dyker in i branded types, lÄt oss klargöra skillnaden mellan strukturell och nominell typning.
Strukturell typning (Duck Typing)
I ett strukturellt typsystem anses tvÄ typer vara kompatibla om de har samma struktur (dvs. samma egenskaper med samma typer). TypeScript anvÀnder strukturell typning. TÀnk pÄ detta exempel:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript
console.log(vector.x); // Output: 10
Ăven om Point
och Vector
deklareras som distinkta typer, tillÄter TypeScript att ett Point
-objekt tilldelas en Vector
-variabel eftersom de delar samma struktur. Detta kan vara bekvÀmt, men det kan ocksÄ leda till fel om du behöver skilja mellan logiskt olika typer som rÄkar ha samma form. TÀnk till exempel pÄ koordinater för latitud/longitud som av en slump kan matcha skÀrmpixelkoordinater.
Nominell typning
I ett nominellt typsystem anses typer vara kompatibla endast om de har samma namn. Ăven om tvĂ„ typer har samma struktur behandlas de som distinkta om de har olika namn. SprĂ„k som Java och C# anvĂ€nder nominell typning.
Behovet av Branded Types
TypeScript's strukturella typning kan vara problematisk nÀr du behöver sÀkerstÀlla att ett vÀrde tillhör en specifik typ, oavsett dess struktur. TÀnk till exempel pÄ att representera valutor. Du kan ha olika typer för USD och EUR, men bÄda kan representeras som tal. Utan en mekanism för att skilja dem Ät kan du av misstag utföra operationer pÄ fel valuta.
Branded types löser detta problem genom att lÄta dig skapa distinkta typer som Àr strukturellt lika men behandlas som olika av typsystemet. Detta förbÀttrar typsÀkerheten och förhindrar fel som annars skulle kunna slinka igenom.
Implementera Branded Types i TypeScript
Branded types implementeras med hjÀlp av intersection-typer och en unik symbol eller strÀngliteral. Idén Àr att lÀgga till ett "mÀrke" (brand) till en typ som skiljer den frÄn andra typer med samma struktur.
AnvÀnda symboler (rekommenderas)
Att anvÀnda symboler för mÀrkning (branding) Àr generellt att föredra eftersom symboler garanterat Àr unika.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
I det hÀr exemplet Àr USD
och EUR
branded types baserade pÄ number
-typen. Den unika symbolen sÀkerstÀller att dessa typer Àr distinkta. Funktionerna createUSD
och createEUR
anvÀnds för att skapa vÀrden av dessa typer, och funktionen addUSD
accepterar endast USD
-vÀrden. Ett försök att addera ett EUR
-vÀrde till ett USD
-vÀrde kommer att resultera i ett typfel.
AnvÀnda strÀngliteraler
Du kan ocksÄ anvÀnda strÀngliteraler för mÀrkning, Àven om denna metod Àr mindre robust Àn att anvÀnda symboler eftersom strÀngliteraler inte garanterat Àr unika.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
Detta exempel uppnĂ„r samma resultat som det föregĂ„ende, men anvĂ€nder strĂ€ngliteraler istĂ€llet för symboler. Ăven om det Ă€r enklare Ă€r det viktigt att sĂ€kerstĂ€lla att de strĂ€ngliteraler som anvĂ€nds för mĂ€rkning Ă€r unika inom din kodbas.
Praktiska exempel och anvÀndningsfall
Branded types kan tillÀmpas i olika scenarier dÀr du behöver upprÀtthÄlla typsÀkerhet utöver strukturell kompatibilitet.
ID:n
TÀnk dig ett system med olika typer av ID:n, sÄsom UserID
, ProductID
och OrderID
. Alla dessa ID:n kan representeras som tal eller strÀngar, men du vill förhindra oavsiktlig blandning av olika ID-typer.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... fetch user data
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... fetch product data
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("User:", user);
console.log("Product:", product);
// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);
Detta exempel visar hur branded types kan förhindra att ett ProductID
skickas till en funktion som förvÀntar sig ett UserID
, vilket förbÀttrar typsÀkerheten.
DomÀnspecifika vÀrden
Branded types kan ocksÄ vara anvÀndbara för att representera domÀnspecifika vÀrden med begrÀnsningar. Du kan till exempel ha en typ för procentandelar som alltid ska vara mellan 0 och 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Percentage must be between 0 and 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Discounted Price:", discountedPrice);
// Uncommenting the next line will cause an error during runtime
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Detta exempel visar hur man kan upprĂ€tthĂ„lla en begrĂ€nsning pĂ„ vĂ€rdet av en branded type vid körtid. Ăven om typsystemet inte kan garantera att ett Percentage
-vÀrde alltid Àr mellan 0 och 100, kan funktionen createPercentage
upprÀtthÄlla denna begrÀnsning vid körtid. Du kan ocksÄ anvÀnda bibliotek som io-ts för att upprÀtthÄlla validering av branded types vid körtid.
Representationer av datum och tid
Att arbeta med datum och tider kan vara knepigt pÄ grund av olika format och tidszoner. Branded types kan hjÀlpa till att skilja mellan olika representationer av datum och tid.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Invalid UTC date format');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Validate that the date string is in local date format (e.g., YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Invalid local date format');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Perform time zone conversion
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("UTC Date:", utcDate);
console.log("Local Date:", localDate);
} catch (error) {
console.error(error);
}
Detta exempel skiljer mellan UTC- och lokala datum, vilket sÀkerstÀller att du arbetar med rÀtt datum- och tidsrepresentation i olika delar av din applikation. Validering vid körtid sÀkerstÀller att endast korrekt formaterade datumstrÀngar kan tilldelas dessa typer.
BÀsta praxis för att anvÀnda Branded Types
För att effektivt anvÀnda branded types i TypeScript, övervÀg följande bÀsta praxis:
- AnvÀnd symboler för mÀrkning: Symboler ger den starkaste garantin för unikhet, vilket minskar risken för typfel.
- Skapa hjÀlpfunktioner: AnvÀnd hjÀlpfunktioner för att skapa vÀrden av branded types. Detta ger en central punkt för validering och sÀkerstÀller konsekvens.
- TillĂ€mpa validering vid körtid: Ăven om branded types förbĂ€ttrar typsĂ€kerheten, förhindrar de inte att felaktiga vĂ€rden tilldelas vid körtid. AnvĂ€nd validering vid körtid för att upprĂ€tthĂ„lla begrĂ€nsningar.
- Dokumentera Branded Types: Dokumentera tydligt syftet och begrÀnsningarna för varje branded type för att förbÀttra kodens underhÄllbarhet.
- ĂvervĂ€g prestandakonsekvenser: Branded types introducerar en liten overhead pĂ„ grund av intersection-typen och behovet av hjĂ€lpfunktioner. ĂvervĂ€g prestandapĂ„verkan i prestandakritiska delar av din kod.
Fördelar med Branded Types
- FörbÀttrad typsÀkerhet: Förhindrar oavsiktlig blandning av strukturellt lika men logiskt olika typer.
- FörbÀttrad kodtydlighet: Gör koden mer lÀsbar och lÀttare att förstÄ genom att explicit skilja mellan typer.
- Minskade fel: FÄngar potentiella fel vid kompilering, vilket minskar risken för buggar vid körtid.
- Ăkad underhĂ„llbarhet: Gör koden lĂ€ttare att underhĂ„lla och refaktorera genom att erbjuda en tydlig ansvarsfördelning.
Nackdelar med Branded Types
- Ăkad komplexitet: LĂ€gger till komplexitet i kodbasen, sĂ€rskilt nĂ€r man hanterar mĂ„nga branded types.
- Overhead vid körtid: Introducerar en liten overhead vid körtid pÄ grund av behovet av hjÀlpfunktioner och validering.
- Risk för boilerplate: Kan leda till repetitiv kod (boilerplate), sÀrskilt vid skapande och validering av branded types.
Alternativ till Branded Types
Ăven om branded types Ă€r en kraftfull teknik för att uppnĂ„ nominell typning i TypeScript, finns det alternativa metoder som du kan övervĂ€ga.
Opaque Types
Opaque types liknar branded types men erbjuder ett mer explicit sÀtt att dölja den underliggande typen. TypeScript har inget inbyggt stöd för opaque types, men du kan simulera dem med hjÀlp av moduler och privata symboler.
Klasser
Att anvĂ€nda klasser kan erbjuda ett mer objektorienterat tillvĂ€gagĂ„ngssĂ€tt för att definiera distinkta typer. Ăven om klasser Ă€r strukturellt typade i TypeScript, erbjuder de en tydligare ansvarsfördelning och kan anvĂ€ndas för att upprĂ€tthĂ„lla begrĂ€nsningar genom metoder.
Bibliotek som `io-ts` eller `zod`
Dessa bibliotek erbjuder sofistikerad typvalidering vid körtid och kan kombineras med branded types för att sÀkerstÀlla sÀkerhet bÄde vid kompilering och körtid.
Slutsats
TypeScript branded types Ă€r ett vĂ€rdefullt verktyg för att förbĂ€ttra typsĂ€kerhet och kodtydlighet i ett strukturellt typsystem. Genom att lĂ€gga till ett "mĂ€rke" (brand) till en typ kan du upprĂ€tthĂ„lla nominell typning och förhindra oavsiktlig blandning av strukturellt lika men logiskt olika typer. Ăven om branded types introducerar viss komplexitet och overhead, övervĂ€ger fördelarna med förbĂ€ttrad typsĂ€kerhet och kodunderhĂ„llbarhet ofta nackdelarna. ĂvervĂ€g att anvĂ€nda branded types i scenarier dĂ€r du behöver sĂ€kerstĂ€lla att ett vĂ€rde tillhör en specifik typ, oavsett dess struktur.
Genom att förstÄ principerna bakom strukturell och nominell typning, och genom att tillÀmpa bÀsta praxis som beskrivs i denna artikel, kan du effektivt utnyttja branded types för att skriva mer robust och underhÄllbar TypeScript-kod. FrÄn att representera valutor och ID:n till att upprÀtthÄlla domÀnspecifika begrÀnsningar, erbjuder branded types en flexibel och kraftfull mekanism för att förbÀttra typsÀkerheten i dina projekt.
NĂ€r du arbetar med TypeScript, utforska de olika tekniker och bibliotek som finns tillgĂ€ngliga för typvalidering och upprĂ€tthĂ„llande. ĂvervĂ€g att anvĂ€nda branded types tillsammans med bibliotek för validering vid körtid som io-ts
eller zod
för att uppnÄ en heltÀckande strategi för typsÀkerhet.